feat(leaderboard): 脚本改走后端 /api/public/leaderboard,断 prisma 直连 DB#322
feat(leaderboard): 脚本改走后端 /api/public/leaderboard,断 prisma 直连 DB#322longsizhuo merged 2 commits intomainfrom
Conversation
依赖:backend PR InvolutionHell/involutionhell-backend#22 必须先合并 + 部署。 背景 generate-leaderboard.mjs 之前 prisma 直连 Postgres 5432,逼着 DB 端口对公网开放 (且服务端没开 SSL)。本周 Vercel preview build 因为 DATABASE_URL 凭证失效全军覆没。 方案 B:DB 收回内网,脚本走后端 endpoint。 改动 scripts/generate-leaderboard.mjs: - 删 prisma client + pg.Pool 直连,换成 fetch ${BACKEND_URL}/api/public/leaderboard (兼容 LEADERBOARD_API_URL 完整覆盖;都没配则走 https://api.involutionhell.com) - 后端不可达时降级写空数组放行 build (不挂整个 deploy) - 保留所有本地处理逻辑:.source/index.ts → docId→title/url 映射、git log noreply 反推 login、前 100 名 GitHub API 兜底 - 后端响应兼容 ApiResponse 包装和裸数组两种结构 lib/db.ts: - 删除(早就是死代码:grep 验证 0 引用,frontend runtime 不直连 DB) docs/architecture/frontend-backend-separation.md: - 在"文档贡献数据源"小节下补迁移记录 + 环境变量说明 后续清理(不在本 PR) - frontend Vercel env 删 DATABASE_URL(preview/production 都删) - 服务器执行 docker compose up -d postgres 让 5432 收回 127.0.0.1 - Oracle Cloud 安全组关 5432 入站
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
Pull request overview
This PR migrates the build-time leaderboard generation away from direct Postgres access (Prisma/pg) to a backend public API, so the database can be kept private and frontend builds are less brittle.
Changes:
- Update
scripts/generate-leaderboard.mjsto fetch aggregated leaderboard data fromGET /api/public/leaderboard, with a fallback to writing an empty leaderboard when the backend is unreachable. - Remove unused Prisma/DB singleton (
lib/db.ts) to eliminate dead frontend DB linkage. - Document the migration and environment variables in the frontend–backend separation architecture doc.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| scripts/generate-leaderboard.mjs | Switches leaderboard aggregation source from direct DB queries to backend API fetch; keeps local enrichment logic and adds a “backend unreachable” fallback. |
| lib/db.ts | Deletes dead Prisma DB singleton previously used for direct DB connections. |
| docs/architecture/frontend-backend-separation.md | Documents the data-source migration and the new env var setup. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| await ensureParentDir(outputAbs); | ||
| try { | ||
| // 检查是否已经存在,存在则不覆盖(或者为了容错直接写入空的 array 也行) | ||
| await fs.writeFile(outputAbs, "[]", "utf-8"); | ||
| } catch (e) { | ||
| // Ignore | ||
| } catch { | ||
| // ignore | ||
| } | ||
| process.exit(0); |
There was a problem hiding this comment.
在后端不可达的降级分支里,ensureParentDir(outputAbs) 不在 try/catch 内:如果 mkdir 因权限/只读文件系统失败,会直接抛错并让脚本以非 0 退出,和“写空榜单放行 build”的目标冲突。建议把 ensureParentDir + writeFile 放进同一个 try/catch,并明确在失败时的退出策略(例如记录错误但仍 exit 0,或在无法落盘时 exit 1 以避免后续 Next import 因文件缺失报更难定位的错)。
| async function fetchAggregatedFromBackend() { | ||
| console.log( | ||
| `[generate-leaderboard] 拉聚合数据:${LEADERBOARD_API_URL} | Fetching aggregated contributions from backend...`, | ||
| ); | ||
| try { | ||
| const res = await fetch(LEADERBOARD_API_URL, { | ||
| headers: { | ||
| accept: "application/json", | ||
| "user-agent": "InvolutionHell-build/1.0 (generate-leaderboard.mjs)", | ||
| }, | ||
| }); |
There was a problem hiding this comment.
fetchAggregatedFromBackend() 的 fetch() 没有设置超时/AbortController;如果后端网络层卡住(例如连接建立但不返回),构建可能无限期挂住。建议加一个合理的超时(如 5–15s)并在超时后返回 null 触发空榜单降级。
| } catch { | ||
| // ignore | ||
| } | ||
| process.exit(0); |
There was a problem hiding this comment.
降级写空榜单时把 writeFile 异常完全吞掉会掩盖真实故障:脚本 exit 0 但 generated/site-leaderboard.json 可能并未生成,后续 Next import "@/generated/site-leaderboard.json" 会以更晦涩的方式失败。建议至少输出一条明确的 error/warn,并根据是否成功写入决定是否应该继续放行构建。
| } catch { | |
| // ignore | |
| } | |
| process.exit(0); | |
| process.exit(0); | |
| } catch (err) { | |
| console.error( | |
| "[generate-leaderboard] 写入空榜单失败,无法继续放行构建:", | |
| err instanceof Error ? err.stack || err.message : err, | |
| ); | |
| process.exit(1); | |
| } |
- fetch 加 AbortController 15s 超时:防止后端 TCP 建立后不返回时 build 无限挂起 (Vercel build 单步通常 <5min,留 15s 给后端 Caffeine 命中即毫秒,未命中也秒级) - 降级写空榜单的 mkdir + writeFile 放同一 try/catch:任一步失败 exit 1 fail-fast 避免文件不存在但 exit 0 让 deploy "看似正常",后续 Next import 抛 ENOENT 更难定位
故障复盘 PR #322 合并后 prod build 跑 generate-leaderboard.mjs 拿到 Cloudflare 的 403 + "Just a moment..." 挑战页,脚本走 fallback 写空数组上线, 首页 Top Rank / /rank contributors 全空。 根因 api.involutionhell.com 走 Cloudflare,默认 Bot Fight Mode 对短时间内多次 请求或低信誉 IP 段(Vercel build runner)会临时 challenge。当时虽然 UA 含 "build" 关键词加重了被拦概率,但实测换任意 UA 当下都能 200,所以本质是 CF 信誉评分 + 时间窗叠加。 本 PR 脚本 UA 从 "InvolutionHell-build/1.0 (generate-leaderboard.mjs)" 改为标准 Chrome UA,避免任何 "build/script/bot" 关键词触发 CF UA 启发式判定。 跟 backend OgFetchService 的 UA 伪装策略对齐。 长期建议(不在本 PR 范围) 在 Cloudflare 给 api.involutionhell.com/api/public/* 加规则: Action: Skip → "Browser Integrity Check" + "Bot Fight Mode" 让公开 API 永远绕过挑战。需要在 CF dashboard 操作。 修复路径 合并 → CI 触发 prod redeploy → generate-leaderboard 拉到真实 21 条 → 首页 Top Rank / /rank contributors 恢复正常。
…325) (#326) * fix(leaderboard): 脚本 UA 换 Chrome 伪装规避 CF Bot Fight 故障复盘 PR #322 合并后 prod build 跑 generate-leaderboard.mjs 拿到 Cloudflare 的 403 + "Just a moment..." 挑战页,脚本走 fallback 写空数组上线, 首页 Top Rank / /rank contributors 全空。 根因 api.involutionhell.com 走 Cloudflare,默认 Bot Fight Mode 对短时间内多次 请求或低信誉 IP 段(Vercel build runner)会临时 challenge。当时虽然 UA 含 "build" 关键词加重了被拦概率,但实测换任意 UA 当下都能 200,所以本质是 CF 信誉评分 + 时间窗叠加。 本 PR 脚本 UA 从 "InvolutionHell-build/1.0 (generate-leaderboard.mjs)" 改为标准 Chrome UA,避免任何 "build/script/bot" 关键词触发 CF UA 启发式判定。 跟 backend OgFetchService 的 UA 伪装策略对齐。 长期建议(不在本 PR 范围) 在 Cloudflare 给 api.involutionhell.com/api/public/* 加规则: Action: Skip → "Browser Integrity Check" + "Bot Fight Mode" 让公开 API 永远绕过挑战。需要在 CF dashboard 操作。 修复路径 合并 → CI 触发 prod redeploy → generate-leaderboard 拉到真实 21 条 → 首页 Top Rank / /rank contributors 恢复正常。 * fix(leaderboard): fallback 优先保留旧 JSON,避免一次拉失败抹掉好数据 PR #325 自身的 preview build 仍被 CF 403 拦下(log 显示 "Just a moment..."), 说明 UA 伪装救不了——CF 是基于 Vercel runner 的 IP 段信誉评分,跟 UA 无关。 真正的根治是去 CF 给 /api/public/* 加 "Skip Bot Fight" 规则(用户操作)。 本次至少把"一次失败抹好数据"这个二次伤害堵住: - 拉到数据 → 正常生成 - 拉不到 + 旧 JSON 有非空数组 → 保留旧版,warn 日志,exit 0 - 拉不到 + 旧 JSON 空/损坏 → 写空数组兜底(首次 build 不挂) - 拉不到 + 旧 JSON 不存在 → 写空数组兜底 效果: 即便 CF 后续仍偶发拦截,prod 上线的 leaderboard 也只会"维持上一版" 而不是"突然空了"。Top Rank 不会因为一次 build 抖动整块消失。 * fix(leaderboard): UA 改回 InvolutionHell-SSR 让 CF Custom Rule 真正匹配 之前误判 昨天看到 build 拿 403 + "Just a moment..." 时,第一反应是"UA 含 build 关键词 触发 CF UA 启发式",于是把 UA 改成 Chrome 伪装。错了。 实际 CF 配置 api.involutionhell.com 上有一条 Custom Rule: (http.host eq "api.involutionhell.com" and http.user_agent contains "InvolutionHell-SSR") → Skip: Bot Fight Mode / Browser Integrity Check / Managed Rules 也就是说 CF **明确依赖 UA token "InvolutionHell-SSR"** 来识别"自己人"放行。 Chrome 伪装恰恰把这个 token 拿掉,规则不匹配,Vercel runner 仍然按 IP 信誉被 Bot Fight 拦下回 403。 本 PR 脚本 UA 改成 "InvolutionHell-SSR/1.0 (build; generate-leaderboard.mjs; +https://involutionhell.com)" 带上 CF 规则要求的 token。本机实测 200 + 21 条数据正常返回。 效果 合并后 prod build → CF 规则匹配 Skip → 拉到真实数据 → site-leaderboard.json 回到 21 条 → 首页 Top Rank / /rank contributors 恢复显示。 --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
必须先合并并部署:InvolutionHell/involutionhell-backend#22
backend endpoint 没上线时合本 PR,build 会走 fallback(写空榜单),排行榜短暂为空。
背景
`generate-leaderboard.mjs` 之前 prisma 直连 Postgres 5432 拉 `doc_contributors`,逼着 DB 端口对公网开放(且服务端没开 SSL)。本周 Vercel preview build 因为 `DATABASE_URL` 凭证失效全军覆没,触发架构整改。方案 B:DB 收回内网,脚本走后端 endpoint。
改动
`scripts/generate-leaderboard.mjs`
`lib/db.ts`
`docs/architecture/frontend-backend-separation.md`
部署后续清理(不在本 PR)
Test plan